Visualizing and Maintaining the Green Canopy of NYC
Author
Zhuohan Sun
Introduction
This project aims to visualize and analyze New York City’s vast urban forest, focusing on the distribution and diversity of trees maintained by the Department of Parks and Recreation (DPR). Using data from the NYC Department of City Planning and the NYC Street Tree Census, we explore spatial patterns of tree coverage and identify differences across boroughs. The findings will inform a proposal to ensure that all New York City residents equitably share the environmental and health benefits of the city’s green canopy.
Data Acquisition
This project utilizes two main datasets obtained from official NYC OpenData sources. First, the NYC Council District Boundaries shapefile was downloaded from the NYC Department of City Planning, which provides the geographic boundaries for all 51 council districts. Second, the NYC Forestry Tree Points dataset was retrieved from NYC Open Data, containing detailed information on each tree’s species, health condition, and precise geographic location. Together, these datasets form the foundation for spatial analysis, allowing us to examine tree distribution and diversity across council districts and boroughs.
After acquiring both datasets, we performed a spatial join to combine the NYC Street Tree Census points with the City Council District boundaries. This integration allowed each tree to be assigned to its respective council district, providing a foundation for subsequent spatial analysis.
A static map was first created to visualize the distribution of all trees across New York City. However, due to the extremely dense nature of the data—over 700,000 tree points—the static image was difficult to interpret. Therefore, an interactive map was developed using leaflet to more effectively display the distribution of trees across all 51 council districts. Users can zoom in and explore tree locations and patterns interactively, which provides clearer insight into spatial variations across boroughs.
Mapping NYC Trees
Show code
# Load council boundariesnyc_districts <-download_nyc_council_boundaries()nyc_trees <-download_nyc_tree_points()
#Sample 10,000 trees for visualizationnyc_trees_sample <- dplyr::slice_sample(nyc_trees, n =10000)# Create ggplot mapggplot() +geom_sf(data = nyc_districts, fill ="gray95", color ="white", linewidth =0.4) +geom_sf(data = nyc_trees, color ="darkgreen", alpha =0.2, size =0.1) +coord_sf() +labs(title ="Distribution of NYC Trees by Council District",subtitle ="Each green point represents one tree from the NYC Street Tree Census",caption ="Source: NYC Open Data — Forestry Tree Census (hn5i-inap)" ) +theme_minimal() +theme(panel.background =element_rect(fill ="aliceblue"),plot.title =element_text(face ="bold", size =14),plot.subtitle =element_text(size =10),axis.text =element_blank(),axis.ticks =element_blank() )
To evaluate whether the distribution of trees across New York City is balanced, we aggregated the Street Tree Census data by City Council District.This district-level analysis helps identify areas with the highest levels of green coverage and those that are relatively lacking.Such findings can guide future urban planning initiatives aimed at promoting a more equitable distribution of the city’s green resources.
Which council district has the most trees?
Show code
# Assign each tree to its corresponding Council Districttree_district_joined <-st_join(nyc_trees, nyc_districts, join = st_intersects)# Count the total number of trees in each Council Districttree_counts <- tree_district_joined |>st_drop_geometry() |>group_by(CounDist) |>summarise(total_trees =n()) |>arrange(desc(total_trees))head(tree_counts)
# Join the tree counts back to the Council District polygonsnyc_districts_counts <- nyc_districts |>left_join(tree_counts, by ="CounDist")# Create a choropleth map showing total trees by districtggplot(nyc_districts_counts) +geom_sf(aes(fill = total_trees), color ="white", linewidth =0.3) +scale_fill_viridis_c(option ="C", trans ="sqrt") +labs(title ="Number of Trees by NYC Council District",subtitle ="Full NYC Street Tree Census (not sampled)",fill ="Total Trees",caption ="Source: NYC Open Data — Street Tree Census" ) +theme_minimal() +theme(plot.title =element_text(face ="bold", size =14),plot.subtitle =element_text(size =10) )
Show code
top_row <- tree_counts |>slice_max(total_trees, n =1)
In this figure the council district with the most trees is 51, with 70,927 trees.
Which council district has the highest density of trees?
Show code
#Join the tree counts with district polygons and calculatetree_density <- tree_counts |>left_join(st_drop_geometry(nyc_districts), by ="CounDist") |>mutate(tree_density = total_trees / Shape_Area) |>arrange(desc(tree_density))head(tree_density)
# Join density data back to the spatial district polygonsnyc_districts_density <- nyc_districts |>left_join(tree_density, by ="CounDist")# Create a choropleth map of tree densityggplot(nyc_districts_density) +geom_sf(aes(fill = tree_density *1e6), color ="white", linewidth =0.3) +scale_fill_viridis_c(option ="C", name ="Trees per km²") +labs(title ="Tree Density by NYC Council District",subtitle ="Calculated from total tree count and land area (Full NYC Street Tree Census)",caption ="Source: NYC Open Data — Street Tree Census (hn5i-inap)" ) +theme_minimal() +theme(plot.title =element_text(face ="bold", size =14),plot.subtitle =element_text(size =10) )
Show code
dens_row <- tree_density |>slice_max(tree_density, n =1)
This map shows the density of trees across NYC Council Districts. While District 51 has the most trees overall,The highest tree density is in Council District 7 at 282 trees per km².they contain many trees within a limited land area.
Which district has highest fraction of dead trees out of all trees?
Show code
# Identify dead trees and calculate the fraction of dead trees in each districtdead_fraction <- tree_district_joined |>st_drop_geometry() |>mutate(is_dead = tpcondition %in%c("Dead", "DEAD")) |>group_by(CounDist) |>summarise(total_trees =n(),dead_trees =sum(is_dead, na.rm =TRUE),fraction_dead = dead_trees / total_trees ) |>arrange(desc(fraction_dead))nyc_districts_dead <- nyc_districts |>left_join(dead_fraction, by ="CounDist")head(dead_fraction)
dead_row <- dead_fraction |>slice_max(fraction_dead, n =1)
This map shows the share of dead trees within each NYC Council District. Districts shaded in lighter colors have a higher percentage of tree mortality, which can signal environmental stress, limited maintenance resources, or an aging tree canopy. The district with the highest proportion of dead trees is Council District 32 (14.2% of all recorded trees are dead.
Conversely, districts represented by darker shades tend to have healthier tree populations, potentially reflecting stronger maintenance efforts or more recent plantings. Tracking these patterns allows urban foresters to identify areas of concern and prioritize interventions—such as targeted maintenance, removals, and replanting—to support a healthier and more resilient urban forest.
What is the most common tree species in Manhattan?
Show code
# Assign boroughs to each tree based on its council districttree_district_joined <- tree_district_joined |>mutate(Borough =case_when( CounDist %in%1:10~"Manhattan", CounDist %in%11:18~"Bronx", CounDist %in%19:32~"Queens", CounDist %in%33:48~"Brooklyn", CounDist %in%49:51~"Staten Island",TRUE~NA_character_ ) )# Filter for Manhattan and count species occurrencesmanhattan_species <- tree_district_joined |>st_drop_geometry() |>filter(Borough =="Manhattan") |>group_by(genusspecies) |>summarise(count =n()) |>arrange(desc(count))head(manhattan_species)
# A tibble: 6 × 2
genusspecies count
<chr> <int>
1 Gleditsia triacanthos var. inermis - Thornless honeylocust 17311
2 Platanus x acerifolia - London planetree 11592
3 Pyrus calleryana - Callery pear 8793
4 Quercus palustris - pin oak 8106
5 Ginkgo biloba - maidenhair tree 7462
6 Zelkova serrata - Japanese zelkova 5771
Show code
# Filter for Manhattanmanhattan_trees <- tree_district_joined |>filter(Borough =="Manhattan")# Identify top 10 most common speciestop_species <- manhattan_trees |>st_drop_geometry() |>count(genusspecies, sort =TRUE) |>slice_head(n =10) |>pull(genusspecies)# Keep only those top 10 species for mappingmanhattan_top_trees <- manhattan_trees |>filter(genusspecies %in% top_species)# Create a color palette for speciespal <-colorFactor(topo.colors(length(top_species)), manhattan_top_trees$genusspecies)# Build interactive leaflet mapleaflet(manhattan_top_trees) |>addProviderTiles("CartoDB.Positron") |>addCircleMarkers(radius =2,stroke =FALSE,fillOpacity =0.7,color =~pal(genusspecies),popup =~paste0("<b>Species:</b> ", genusspecies, "<br>","<b>Status:</b> ", tpcondition ) ) |>addLegend("bottomright",pal = pal,values =~genusspecies,title ="Top 10 Tree Species",opacity =1 )